package com.bugsnag.ohos;

import com.bugsnag.ohos.util.OhosLog;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Metadata
 *
 * @since 2021-07-01
 */
public class Metadata implements JsonStream.Streamable, MetadataAware {
    Map<String, Map<String, Object>> store;
    ObjectJsonStreamer jsonStreamer = new ObjectJsonStreamer();
    Set<String> redactedKeys;

    /**
     * constructor
     */
    public Metadata() {
        this(new ConcurrentHashMap());
    }

    /**
     * constructor
     *
     * @param store
     */
    public Metadata(Map<String, Map<String, Object>> store) {
        this.store = store;
    }

    public Set<String> getRedactedKeys() {
        return jsonStreamer.getRedactedKeys();
    }

    /**
     * set Redacted Keys
     *
     * @param redactedKeys
     */
    public void setRedactedKeys(Set<String> redactedKeys) {
        jsonStreamer.setRedactedKeys(redactedKeys);
    }

    @Override
    public void toStream(JsonStream writer) throws IOException {
        jsonStreamer.objectToStream(store, writer, true);
    }

    @Override
    public void addMetadata(String section, Map<String, Object> value) {
        value.entrySet().forEach(stringObjectEntry -> addMetadata(section,
            stringObjectEntry.getKey(), stringObjectEntry.getValue()));
    }

    @Override
    public void addMetadata(String section, String key, Object value) {
        if (value == null) {
            clearMetadata(section, key);
        } else {
            Map<String, Object> map = store.get(section);
            Map<String, Object> tab = map != null ? map : new ConcurrentHashMap();
            store.put(section, tab);
            insertValue(tab, key, value);
        }
    }

    private void insertValue(Map<String, Object> map, String key, Object newValue) {
        Object obj = newValue;
        Object existingValue = map.get(key);
        if (existingValue != null && obj instanceof Map) {
            List maps = new ArrayList();
            maps.add(existingValue);
            maps.add(newValue);
            obj = mergeMaps(maps);
        }
        map.put(key, obj);
    }

    @Override
    public void clearMetadata(String section) {
        store.remove(section);
    }

    @Override
    public void clearMetadata(String section, String key) {
        Map<String, Object> tab = store.get(section);
        tab.remove(key);
        if (tab.isEmpty()) {
            store.remove(section);
        }
    }

    @Override
    public Map<String, Object> getMetadata(String section) {
        return store.get(section);
    }

    @Override
    public Object getMetadata(String section, String key) {
        if (getMetadata(section) != null) {
            return getMetadata(section).get(key);
        }
        return null;
    }

    /**
     * to map
     *
     * @return map data
     */
    public Map<String, Map<String, Object>> toMap() {
        ConcurrentHashMap copy = new ConcurrentHashMap(store);
        for (String key : store.keySet()) {
            copy.put(key, new ConcurrentHashMap(store.get(key)));
        }
        return copy;
    }

    /**
     * copy
     *
     * @return meta data
     */
    public Metadata copy() {
        Metadata metadata = new Metadata(toMap());
        metadata.redactedKeys = redactedKeys;
        return metadata;
    }

    /**
     * merge
     *
     * @param data
     * @return meta data
     */
    public static Metadata merge(Metadata... data) {
        List<Map<String, Object>> stores = new ArrayList();
        Set<String> redactKeys = new HashSet<>();
        for (Metadata datum : data) {
            if (datum.jsonStreamer.getRedactedKeys() != null) {
                for (String redactedKey : datum.jsonStreamer.getRedactedKeys()) {
                    redactKeys.add(redactedKey);
                }
            }
            Map<String, Object> toMap = (Map) datum.toMap();
            stores.add(toMap);
        }
        Map<String, Object> stringObjectMap = mergeMaps(stores);
        Map<String, Map<String, Object>> newStore = new HashMap<>();
        for (String key : stringObjectMap.keySet()) {
            newStore.put(key, (Map<String, Object>) stringObjectMap.get(key));
        }
        Metadata newMeta = new Metadata(newStore);
        newMeta.redactedKeys = redactKeys;
        return newMeta;
    }

    /**
     * merge maps
     *
     * @param data
     * @return maps data
     */
    public static Map<String, Object> mergeMaps(List<Map<String, Object>> data) {
        Set<String> keys = new HashSet<>();
        for (Map<String, Object> map : data) {
            for (String key : map.keySet()) {
                keys.add(key);
            }
        }
        Map result = new ConcurrentHashMap<String, Object>();
        for (Map<String, Object> map : data) {
            for (String key : keys) {
                getMergeValue(result, key, map);
            }
        }
        return result;
    }

    private static void getMergeValue(Map<String, Object> result, String key, Map<String, Object> map) {
        OhosLog.d("Metadata", " getMergeValue getMergeValue: %s");
        Object baseValue = result.get(key);
        Object overridesValue = map.get(key);
        if (overridesValue != null) {
            if (baseValue instanceof Map && overridesValue instanceof Map) {
                Map<String, Object> first = (Map<String, Object>) baseValue;
                Map<String, Object> second = (Map<String, Object>) overridesValue;
                List maps = new ArrayList();
                maps.add(first);
                maps.add(second);
                result.put(key, mergeMaps(maps));
            } else {
                result.put(key, overridesValue);
            }
        } else {
            if (baseValue != null) {
                result.put(key, baseValue);
            }
        }
    }
}
